# Copyright © The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""
Default Django settings for the Debusine project.

Most settings are documented in this file and they are initialized to some
reasonable default values when possible.  They will be extended (and
possibly overridden) by settings from the other modules in this package
depending on the setup selected by the administrator. You likely won't
have to modify that file.

You should instead create local.py to put your site-specific settings (use
local.py.sample as template).

Here are the most important settings:

:py:data:`DEBUSINE_FQDN`
    The fully qualified domain name of the debusine installation.
    It should be a service-specific DNS entry like "debusine.example.com".
    Defaults to the FQDN of the machine which might not be adequate.

:py:data:`DEBUSINE_DATA_PATH`
    The directory where debusine will hold its data. The directory is
    further sub-divided in multiple directories for specific use
    cases (e.g. cache, keyring, static, media, logs, templates, etc.).
    Defaults to the "data" sub-directory in the debusine
    base directory (where the code lives).

:py:data:`MEDIA_URL`
    URL that handles the media served from MEDIA_ROOT. Make sure to use a
    trailing slash.
    Examples: "http://example.com/media/", "http://media.example.com/"
    Defaults to "/media/".

:py:data:`STATIC_URL`
    URL prefix for static files.
    Example: "http://example.com/static/", "http://static.example.com/"
    Defaults to "/static/"

Some settings have default values which are computed dynamically from
other settings. Those settings can also be overridden. Here's the list
of those settings.

py:data:`STATIC_ROOT`
    Absolute path to the directory static files should be collected to.
    Don't put anything in this directory yourself; store your static files
    in apps' "static/" subdirectories and in STATICFILES_DIRS. Defaults
    to the "static" sub-directory of :py:data:`DEBUSINE_DATA_PATH`.

:py:data:`MEDIA_ROOT`
    Absolute filesystem path to the directory that will hold user-uploaded
    files. Defaults to the "media" sub-directory of
    :py:data:`DEBUSINE_DATA_PATH`.

:py:data:`DEBUSINE_CACHE_DIRECTORY`
    This directory is used to store the locally cached resources.
    Any Debusine app should be able to use this directory to store
    its caches. For example, it is used to store the APT cache of repository
    information and the cache of retrieved Web resources.
    Defaults to the "cache" sub-directory of
    :py:data:`DEBUSINE_DATA_PATH`.

:py:data:`DEBUSINE_LOG_DIRECTORY`
    This directory will hold log files generated by debusine.
    Defaults to the "logs" sub-directory of py:data:`DEBUSINE_DATA_PATH`.

:py:data:`DEBUSINE_TEMPLATE_DIRECTORY`
    This directory can hold custom templates that will override the
    templates supplied by debusine. Defaults to the "templates"
    sub-directory of py:data:`DEBUSINE_DATA_PATH`.

:py:data:`DEBUSINE_UPLOAD_DIRECTORY`
    This directory temporarily holds files being uploaded to debusine.
    Defaults to the "uploads" sub-directory of py:data:`DEBUSINE_DATA_PATH`.

:py:data:`DEBUSINE_STORE_DIRECTORY`
    This directory is used by the default LocalFileBackend. Defaults to the
    "store" sub-directory of py:data:`DEBUSINE_DATA_PATH`.

More settings:

"""
import os.path
import socket
from datetime import timedelta
from os.path import dirname
from typing import Any, TYPE_CHECKING

import django
from django.core.exceptions import ImproperlyConfigured

if TYPE_CHECKING:
    from nacl.public import PrivateKey
else:
    PrivateKey = object

if django.VERSION < (4, 2):
    raise ImproperlyConfigured("Debusine needs Django >= 4.2")

# Django's debug mode, never enable this in production
DEBUG = False

BASE_DIR = dirname(dirname(dirname(dirname(__file__))))
DEBUSINE_DATA_PATH = os.path.join(BASE_DIR, 'data')

DEBUSINE_CACHE_DIRECTORY: str
DEBUSINE_TEMPLATE_DIRECTORY: str
DEBUSINE_LOG_DIRECTORY: str
DEBUSINE_UPLOAD_DIRECTORY: str
DEBUSINE_STORE_DIRECTORY: str

# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone.
TIME_ZONE = 'Etc/UTC'

# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us'

# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True

# If you set this to False, Django will not use timezone-aware datetimes.
USE_TZ = True

# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://example.com/media/", "http://media.example.com/"
MEDIA_URL = '/media/'

# URL prefix for static files.
# Example: "http://example.com/static/", "http://static.example.com/"
STATIC_URL = '/static/'

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

# List of finder classes that know how to find static files in
# various locations.
STATICFILES_FINDERS = [
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
    # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
]


STATICFILES_DIRS = [
    ("vendor/select2.js", "/usr/share/javascript/select2.js"),
    ("vendor/jquery", "/usr/share/javascript/jquery"),
]

# Overridden in production.py by content of /var/lib/debusine/server/key
SECRET_KEY = "default:i77$9ld7x1%h1&_xpdjwy7i-jjaox@-ybtpe#g&8gw@yl4-1%&"

# Templating rules
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                # 'django.template.context_processors.i18n',
                # 'django.template.context_processors.media',
                'django.template.context_processors.static',
                # 'django.template.context_processors.tz',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'debusine.web.context_processors.server_info',
                'debusine.web.context_processors.application_context',
            ]
        },
    }
]

MIDDLEWARE: list[str] = [
    # Introduces correct scoping for contextvars
    'debusine.server.middlewares.context.ContextMiddleware',
    'debusine.server.middlewares.headers.HeadersMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'debusine.server.middlewares.scopes.ScopeMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    # Needs to stay right after AuthenticationMiddleware
    'debusine.server.signon.middleware.SignonMiddleware',
    'debusine.server.middlewares.scopes.AuthorizationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    # Disabled to allow rendering in iframes
    # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

AUTHENTICATION_BACKENDS = [
    'debusine.server.signon.auth.SignonAuthBackend',
]

LOGIN_REDIRECT_URL = "homepage:homepage"

# AUTHENTICATION_BACKENDS = [
#    'django_email_accounts.auth.UserEmailBackend',
# ]
#
# AUTH_USER_MODEL = 'accounts.User'

ROOT_URLCONF = 'debusine.project.urls'

# Python dotted path to the WSGI application used by Django's runserver.
WSGI_APPLICATION = 'debusine.project.wsgi.application'

# Python dotted path to the ASGI application used by Django's runserver.
ASGI_APPLICATION = "debusine.project.asgi.application"

# This is required to make request.is_secure() work behind a TLS-terminating
# proxy.
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

INSTALLED_APPS: list[str] = [
    'daphne',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_celery_results',
    'channels',
    'pgtrigger',
    'rest_framework',
    'debusine.db',
    'debusine.server',
    'debusine.web',
]

# This will need to be tightened up later.
PAGE_CLASS = 'DEFAULT_PAGINATION_CLASS'
REST_FRAMEWORK = {
    PAGE_CLASS: 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
    "EXCEPTION_HANDLER": "debusine.server.exceptions."
    "debusine_exception_handler",
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "debusine.server.views.auth.DebusineTokenAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ],
}

# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': (
            'django.contrib.auth.password_validation.'
            'UserAttributeSimilarityValidator'
        )
    },
    {
        'NAME': (
            'django.contrib.auth.password_validation.MinimumLengthValidator'
        )
    },
    {
        'NAME': (
            'django.contrib.auth.password_validation.CommonPasswordValidator'
        )
    },
    {
        'NAME': (
            'django.contrib.auth.password_validation.NumericPasswordValidator'
        )
    },
]

# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '%(asctime)s [%(module)s/%(process)d/%(thread)d] '
            + '%(levelname)s: %(message)s'
        },
        'standard': {
            'format': '%(asctime)s %(process)d %(levelname)s: %(message)s'
        },
        'simple': {'format': '%(asctime)s %(levelname)s: %(message)s'},
    },
    'filters': {
        'require_debug_false': {'()': 'django.utils.log.RequireDebugFalse'},
        'require_debug_true': {'()': 'django.utils.log.RequireDebugTrue'},
    },
    'handlers': {
        'console': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'formatter': 'simple',
            'filters': ['require_debug_true'],
        },
        'mail_admins': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],
            'class': 'django.utils.log.AdminEmailHandler',
        },
        'null': {'level': 'DEBUG', 'class': 'logging.NullHandler'},
        'mail.log': {
            'level': 'INFO',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': 'mail.log',
            'encoding': 'utf-8',
            'formatter': 'standard',
            'when': 'W0',
            'backupCount': 52,
        },
        'tasks.log': {
            'level': 'INFO',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': 'tasks.log',
            'encoding': 'utf-8',
            'formatter': 'standard',
            'when': 'W0',
            'backupCount': 52,
        },
        'errors.log': {
            'level': 'WARNING',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': 'errors.log',
            'encoding': 'utf-8',
            'formatter': 'verbose',
            'when': 'W0',
            'backupCount': 52,
        },
        'debug.log': {
            'level': 'DEBUG',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': 'debug.log',
            'encoding': 'utf-8',
            'formatter': 'verbose',
            'when': 'W0',
            'backupCount': 52,
        },
    },
    'loggers': {
        'root': {
            'handlers': ['errors.log'],
            'level': 'ERROR',
            'propagate': False,
        },
        'django.request': {
            'handlers': ['errors.log', 'mail_admins'],
            'level': 'ERROR',
            'propagate': False,
        },
        'django.security': {
            'handlers': ['errors.log', 'mail_admins'],
            'level': 'ERROR',
            'propagate': False,
        },
        'django.security.DisallowedHost': {
            'handlers': ['errors.log'],
            'level': 'ERROR',
            'propagate': False,
        },
        'py.warnings': {'handlers': ['console']},
        'debusine': {
            'handlers': ['debug.log', 'errors.log', 'console'],
            'level': 'DEBUG',
            'propagate': False,
        },
        'debusine.mail': {
            'handlers': ['mail.log', 'mail_admins'],
            'level': 'DEBUG',
            'propagate': True,
        },
        'debusine.tasks': {
            'handlers': ['tasks.log', 'mail_admins'],
            'level': 'DEBUG',
            'propagate': True,
        },
    },
}

AUTH_USER_MODEL = "db.User"

CELERY_BEAT_SCHEDULE = {
    # Run the scheduler once every five minutes, just in case we have a bug
    # where something forgot to request a run of the scheduler when data
    # changed.
    "fallback-scheduler": {
        "task": "debusine.server.scheduler.schedule_task",
        "schedule": timedelta(minutes=5),
    },
    "provisioner": {
        "task": "debusine.server.provisioning.provision",
        "schedule": timedelta(minutes=1),
    },
}
CELERY_BROKER_URL = "redis://localhost:6379"
CELERY_RESULT_BACKEND = "django-db"
CELERY_RESULT_BACKEND_TRANSPORT_OPTIONS = {"global_keyprefix": "celery"}
CELERY_TASK_ROUTES = {
    "debusine.server.scheduler.schedule_task": {"queue": "scheduler"},
    "debusine.server.provisioning.provision": {"queue": "provisioner"},
}
CELERY_WORKER_PREFETCH_MULTIPLIER = 1

EMAIL_TIMEOUT = 5

# === Debusine specific settings ===

# The fully qualified domain name for the Debusine deployment
DEBUSINE_FQDN: str = socket.getfqdn()

# Default CHANNEL_LAYERS pointing at a Redis local configuration
CHANNEL_LAYERS: dict[str, dict[str, Any]] = {
    "default": {
        "BACKEND": "channels_redis.pubsub.RedisPubSubChannelLayer",
        "CONFIG": {"hosts": ["redis://localhost:6379"]},
    }
}

# A default contact email
DEBUSINE_CONTACT_EMAIL = f'owner@{DEBUSINE_FQDN}'

# Name of the default scope to use for legacy web requests and API calls that
# are unaware of scopes
DEBUSINE_DEFAULT_SCOPE = 'debusine'
# Name of the default workspace to use for migrations, API calls that do not
# specify the workspace, management commands, and so on.
DEBUSINE_DEFAULT_WORKSPACE = "System"

# Encryption keys used by the signing service to encrypt private keys; the
# first in the list is used for newly-encrypted private keys, while the
# others may also be used for decryption
DEBUSINE_SIGNING_PRIVATE_KEYS: list[PrivateKey] = []

# Allow signing objects that declare a trust chain to these certificate
# fingerprints.  See
# https://wiki.debian.org/SecureBoot/Discussion#Describing_the_trust_chain.
DEBUSINE_SIGNING_TRUSTED_CERTS: list[str] = []

# The lambda functions are evaluated at the end of the settings import
# logic. They provide default values to settings which have not yet been
# set (neither above nor in local.py).
_COMPUTE_DEFAULT_SETTINGS = (
    ('ALLOWED_HOSTS', lambda t: [t['DEBUSINE_FQDN'], 'localhost', '127.0.0.1']),
    ('ADMINS', lambda t: (('Debusine Admins', t['DEBUSINE_CONTACT_EMAIL']),)),
    ('SERVER_EMAIL', lambda t: t['DEBUSINE_CONTACT_EMAIL']),
    ('DEFAULT_FROM_EMAIL', lambda t: t['DEBUSINE_CONTACT_EMAIL']),
    ('STATIC_ROOT', lambda t: os.path.join(t['DEBUSINE_DATA_PATH'], 'static')),
    ('MEDIA_ROOT', lambda t: os.path.join(t['DEBUSINE_DATA_PATH'], 'media')),
    (
        'DEBUSINE_CACHE_DIRECTORY',
        lambda t: os.path.join(t['DEBUSINE_DATA_PATH'], 'cache'),
    ),
    (
        'DEBUSINE_TEMPLATE_DIRECTORY',
        lambda t: os.path.join(t['DEBUSINE_DATA_PATH'], 'templates'),
    ),
    (
        'DEBUSINE_LOG_DIRECTORY',
        lambda t: os.path.join(t['DEBUSINE_DATA_PATH'], 'logs'),
    ),
    (
        'DEBUSINE_UPLOAD_DIRECTORY',
        lambda t: os.path.join(t['DEBUSINE_DATA_PATH'], 'uploads'),
    ),
    (
        'DEBUSINE_STORE_DIRECTORY',
        lambda t: os.path.join(t['DEBUSINE_DATA_PATH'], 'store'),
    ),
)


def compute_default_settings(target: dict[str, Any]) -> None:
    """
    Dynamically generate some default settings.

    There are many settings whose default value depends on another
    setting. They are defined in the _COMPUTE_DEFAULT_SETTINGS dict
    with a function that evaluates them.
    """
    for setting, value in _COMPUTE_DEFAULT_SETTINGS:
        if setting in target:
            continue  # Settings is already defined
        target[setting] = value(target)
    # Extend TEMPLATE_DIRS with our directory
    target['TEMPLATES'][0]['DIRS'].append(target['DEBUSINE_TEMPLATE_DIRECTORY'])
    # Update LOGGING with full paths
    for handler in target['LOGGING']['handlers'].values():
        if 'filename' not in handler or "/" in handler['filename']:
            continue
        handler['filename'] = os.path.join(
            target['DEBUSINE_LOG_DIRECTORY'], handler['filename']
        )
    # Update DATABASES with full paths
    dbconf = target['DATABASES']['default']
    if dbconf['ENGINE'] == 'django.db.backends.sqlite3':
        if '/' not in dbconf['NAME']:
            dbconf['NAME'] = os.path.join(
                target['DEBUSINE_DATA_PATH'], dbconf['NAME']
            )
        if (
            'TEST' in dbconf
            and 'NAME' in dbconf['TEST']
            and '/' not in dbconf['TEST']['NAME']
        ):
            dbconf['TEST']['NAME'] = os.path.join(
                target['DEBUSINE_DATA_PATH'], dbconf['TEST']['NAME']
            )


def test_data_override(config: dict[str, Any]) -> None:
    """
    Setup temp data directories for testing.

    This is called through LOGGING_CONFIG, because that hooks is
    invoked after the settings are processed (including
    .__init__.compute_default_settings()), right before the
    application starts.
    """
    import logging.config

    from django.conf import settings

    from debusine.server.management.commands import test

    test_mode = getattr(settings, 'TEST_MODE', False)
    if test_mode:
        # Replace and create DEBUSINE_*_DIRECTORY
        test.setup_temp_data_directories()

        # Divert log handlers to new temp DEBUSINE_LOG_DIRECTORY
        for handler in config['handlers'].values():
            if 'filename' in handler:
                handler['filename'] = os.path.join(
                    settings.DEBUSINE_LOG_DIRECTORY,
                    os.path.basename(handler['filename']),
                )

    # default logging config handler
    logging.config.dictConfig(config)


LOGGING_CONFIG = 'debusine.project.settings.defaults.test_data_override'
